(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
      define(factory);
    } else if (typeof exports === 'object') {
      module.exports = factory();
    } else {
      root.way = factory();
    }
})(this, function() {
    'use strict';
    var way, w, tagPrefix = 'way';
  
    //////////////////////////////
    // EVENT EMITTER DEFINITION //
    //////////////////////////////
  
    var EventEmitter = function() {
      this._watchers = {};
      this._watchersAll = {};
    };
  
    EventEmitter.prototype.constructor = EventEmitter;
  
    EventEmitter.prototype.watchAll = function(handler) {
      this._watchersAll = this._watchersAll || [];
      if (!_w.contains(this._watchersAll, handler)) {
        this._watchersAll.push(handler);
      }
    };
  
    EventEmitter.prototype.watch = function(selector, handler) {
      if (!this._watchers) {
        this._watchers = {};
      }
      this._watchers[selector] = this._watchers[selector] || [];
      this._watchers[selector].push(handler);
    };
  
    EventEmitter.prototype.findWatcherDeps = function(selector) {
      // Go up to look for parent watchers
      // ex: if "some.nested.value" is the selector, it should also trigger for "some"
  
      var result = [];
      var watchers = _w.keys(this._watchers);
      watchers.forEach(function(watcher) {
        if (startsWith(selector, watcher)) {
          result.push(watcher);
        }
      });
      return result;
    };
  
    EventEmitter.prototype.emitChange = function(selector /* , arguments */) {
      if (!this._watchers) {
        this._watchers = {};
      }
  
      var self = this;
  
      // Send data down to the local watchers
      var deps = self.findWatcherDeps(selector);
      deps.forEach(function(item) {
        if (self._watchers[item]) {
          self._watchers[item].forEach(function(handler) {
            handler.apply(self, [self.get(item)]);
          });
        }
      });
  
      // Send data down to the global watchers
      if (!self._watchersAll || !_w.isArray(self._watchersAll)) {
        return;
      }
      self._watchersAll.forEach(function(watcher) {
        if (_w.isFunction(watcher)) {
          watcher.apply(self, [selector, self.get(selector)]);
        }
      });
    };
  
    ////////////////////
    // WAY DEFINITION //
    ////////////////////
  
    var WAY = function() {
      this.data = {};
      this._bindings = {};
      this.options = {
        persistent: true,
        timeoutInput: 50,
        timeoutDOM: 500
      };
    };
  
    // Inherit from EventEmitter
    WAY.prototype = Object.create(EventEmitter.prototype);
    WAY.constructor = WAY;
  
    //////////////////////////
    // DOM METHODS CHAINING //
    //////////////////////////
  
    WAY.prototype.dom = function(element) {
      this._element = w.dom(element).get(0);
      return this;
    };
  
    //////////////////////////////
    // DOM METHODS: DOM -> JSON //
    //////////////////////////////
  
    WAY.prototype.toStorage = function(options, element) {
      var self = this,
        element = element || self._element,
        options = options || self.dom(element).getOptions(),
        data = self.dom(element).toJSON(options),
        scope = self.dom(element).scope(),
        selector = scope ? scope + '.' + options.data : options.data;
  
      if (options.readonly) {
        return false;
      }
      self.set(selector, data, options);
    };
  
    WAY.prototype.toJSON = function(options, element) {
      var self = this,
        element = element || self._element,
        data = self.dom(element).getValue(),
        options = options || self.dom(element).getOptions();
  
      if (_w.isArray(options.pick)) {
        data = selectNested(data, options.pick, true);
      }
      if (_w.isArray(options.omit)) {
        data = selectNested(data, options.omit, false);
      }
  
      return data;
    };
  
    //////////////////////////////
    // DOM METHODS: JSON -> DOM //
    //////////////////////////////
  
    WAY.prototype.fromStorage = function(options, element) {
      var self = this,
        element = element || self._element,
        options = options || self.dom(element).getOptions();
  
      if (options.writeonly) {
        return false;
      }
  
      var scope = self.dom(element).scope(),
        selector = scope ? scope + '.' + options.data : options.data,
        data = self.get(selector);
  
      self.dom(element).fromJSON(data, options);
    };
  
    WAY.prototype.fromJSON = function(data, options, element) {
      var self = this,
        element = element || self._element,
        options = options || self.dom(element).getOptions();
  
      if (options.writeonly) {
        return false;
      }
  
      if (_w.isObject(data)) {
        if (_w.isArray(options.pick)) {
          data = selectNested(data, options.pick, true);
        }
        if (_w.isArray(options.omit)) {
          data = selectNested(data, options.omit, false);
        }
        var currentData = _w.isObject(self.dom(element).toJSON())
          ? self.dom(element).toJSON()
          : {};
        data = _w.extend(currentData, data);
      }
  
      if (options.json) {
        data = _json.isStringified(data) ? data : _json.prettyprint(data);
      }
  
      self.dom(element).setValue(data, options);
    };
  
    /////////////////////////////////
    // DOM METHODS: GET - SET HTML //
    /////////////////////////////////
  
    WAY.prototype.getValue = function(element) {
      var self = this, element = element || self._element;
  
      var getters = {
        SELECT: function() {
          return w.dom(element).val();
        },
        INPUT: function() {
          var type = w.dom(element).type();
          if (_w.contains(['checkbox', 'radio'], type)) {
            return w.dom(element).prop('checked') ? w.dom(element).val() : null;
          }else{
            return w.dom(element).val();
          }
        },
        TEXTAREA: function() {
          return w.dom(element).val();
        }
      };
      var defaultGetter = function(a) {
        return w.dom(element).html();
      };
  
      var elementType = w.dom(element).get(0).tagName;
      var getter = getters[elementType] || defaultGetter;
      return getter();
    };
  
    WAY.prototype._transforms = {
      uppercase: function(data) {
        return _w.isString(data) ? data.toUpperCase() : data;
      },
      lowercase: function(data) {
        return _w.isString(data) ? data.toLowerCase() : data;
      },
      reverse: function(data) {
        return data && data.split && _w.isFunction(data.split)
          ? data.split('').reverse().join('')
          : data;
      }
    };
  
    WAY.prototype.registerTransform = function(name, transform) {
      var self = this;
      if (_w.isFunction(transform)) {
        self._transforms[name] = transform;
      }
    };
  
    WAY.prototype.setValue = function(data, options, element) {
      var self = this,
        element = element || self._element,
        options = options || self.dom(element).getOptions();
  
      options.transform = options.transform || [];
      options.transform.forEach(function(transformName) {
        var transform =
          self._transforms[transformName] ||
          function(data) {
            return data;
          };
        data = transform(data);
      });
  
      var setters = {
        SELECT: function(a) {
          w.dom(element).val(a);
        },
        INPUT: function(a) {
          if (!_w.isString(a)) {
            a = JSON.stringify(a);
          }
          var type = w.dom(element).get(0).type;
          if (_w.contains(['text', 'password', 'number'], type)) {
            w.dom(element).val(a || '');
          }
          if (_w.contains(['checkbox', 'radio'], type)) {
            if (a === w.dom(element).val()) {
              w.dom(element).prop('checked', true);
            } else {
              w.dom(element).prop('checked', false);
            }
          }
        },
        TEXTAREA: function(a) {
          if (!_w.isString(a)) {
            a = JSON.stringify(a);
          }
          w.dom(element).val(a || '');
        },
        PRE: function(a) {
          if (options.html) {
            w.dom(element).html(a);
          } else {
            w.dom(element).text(a);
          }
        },
        IMG: function(a) {
          if (!a) {
            a = options.default || '';
            w.dom(element).attr('src', a);
            return false;
          }
  
          var isValidImageUrl = function(url, cb) {
            w.dom(element).addClass('way-loading');
            w.dom('img', {
              src: url,
              onerror: function() {
                cb(false);
              },
              onload: function() {
                cb(true);
              }
            });
          };
  
          isValidImageUrl(a, function(response) {
            w.dom(element).removeClass('way-loading');
            if (response) {
              w.dom(element).removeClass('way-error').addClass('way-success');
            } else {
              if (a) {
                w.dom(element).addClass('way-error');
              } else {
                w
                  .dom(element)
                  .removeClass('way-error')
                  .removeClass('way-success');
              }
              a = options.default || '';
            }
            w.dom(element).attr('src', a);
          });
        }
      };
      var defaultSetter = function(a) {
        if (options.html) {
          w.dom(element).html(a);
        } else {
          w.dom(element).text(a);
        }
      };
  
      var elementType = w.dom(element).get(0).tagName;
      var setter = setters[elementType] || defaultSetter;
      setter(data);
    };
  
    WAY.prototype.setDefault = function(force, options, element) {
      var self = this,
        element = element || self._element,
        force = force || false,
        options = options
          ? _w.extend(self.dom(element).getOptions(), options)
          : self.dom(element).getOptions();
  
      // Should we just set the default value in the DOM, or also in the datastore?
      if (!options.default) {
        return false;
      }
      if (force) {
        self.set(options.data, options.default, options);
      } else {
        self.dom(element).setValue(options.default, options);
      }
    };
  
    WAY.prototype.setDefaults = function() {
      var self = this, dataSelector = '[' + tagPrefix + '-default]';
  
      var elements = w.dom(dataSelector).get();
      for (var i in elements) {
        var element = elements[i],
          options = self.dom(element).getOptions(),
          selector = options.data || null,
          data = selector ? self.get(selector) : null;
        if (!data) {
          self.dom(element).setDefault();
        }
      }
    };
  
    /////////////////////////////////////
    // DOM METHODS: GET - SET BINDINGS //
    /////////////////////////////////////
  
    // Scans the DOM to look for new bindings
    WAY.prototype.registerBindings = function() {
      // Dealing with bindings removed from the DOM by just resetting all the bindings all the time.
      // Isn't there a better way?
      // One idea would be to add a "way-bound" class to bound elements
      // self._bindings = {};
  
      var self = this;
      var selector = '[' + tagPrefix + '-data]';
      self._bindings = {};
  
      var elements = w.dom(selector).get();
      for (var i in elements) {
        var element = elements[i],
          options = self.dom(element).getOptions(),
          scope = self.dom(element).scope(),
          selector = scope ? scope + '.' + options.data : options.data;
  
        self._bindings[selector] = self._bindings[selector] || [];
        if (!_w.contains(self._bindings[selector], w.dom(element).get(0))) {
          self._bindings[selector].push(w.dom(element).get(0));
        }
      }
    };
  
    WAY.prototype.updateBindings = function(selector) {
      var self = this;
      self._bindings = self._bindings || {};
  
      // Set bindings for the data selector
      var bindings = pickAndMergeParentArrays(self._bindings, selector);
      bindings.forEach(function(element) {
        var focused = w.dom(element).get(0) === w.dom(':focus').get(0)
          ? true
          : false;
        if (!focused) {
          self.dom(element).fromStorage();
        }
      });
  
      // Set bindings for the global selector
      if (self._bindings['__all__']) {
        self._bindings['__all__'].forEach(function(element) {
          self.dom(element).fromJSON(self.data);
        });
      }
    };
  
    ////////////////////////////////////
    // DOM METHODS: GET - SET REPEATS //
    ////////////////////////////////////
  
    WAY.prototype.registerRepeats = function() {
      // Register repeats
      var self = this;
      var selector = '[' + tagPrefix + '-repeat]';
      self._repeats = self._repeats || {};
      self._repeatsCount = self._repeatsCount || 0;
  
      var elements = w.dom(selector).get();
      for (var i in elements) {
        var element = elements[i], options = self.dom(element).getOptions();
  
        self._repeats[options.repeat] = self._repeats[options.repeat] || [];
  
        var wrapperAttr =
          tagPrefix + '-repeat-wrapper="' + self._repeatsCount + '"',
          parent = w.dom(element).parent('[' + wrapperAttr + ']');
        if (!parent.length) {
          self._repeats[options.repeat].push({
            id: self._repeatsCount,
            element: w
              .dom(element)
              .clone(true)
              .removeAttr(tagPrefix + '-repeat')
              .removeAttr(tagPrefix + '-filter')
              .get(0),
            selector: options.repeat,
            filter: options.filter
          });
  
          var wrapper = document.createElement('div');
          w.dom(wrapper).attr(tagPrefix + '-repeat-wrapper', self._repeatsCount);
          w.dom(wrapper).attr(tagPrefix + '-scope', options.repeat);
          if (options.filter) {
            w.dom(wrapper).attr(tagPrefix + '-filter', options.filter);
          }
  
          w.dom(element).replaceWith(wrapper);
          self.updateRepeats(options.repeat);
  
          self._repeatsCount++;
        }
      }
    };
  
    /*
      WAY.prototype._filters = {
          noFalsy: function(item ) {
              if (!item) {
                  return false;
              } else {
                  return true;
              }
          }
      };
  
      WAY.prototype.registerFilter = function(name, filter) {
          var self = this;
          if (_w.isFunction(filter)) { self._filters[name] = filter; }
      }
      */
  
    WAY.prototype.updateRepeats = function(selector) {
      var self = this;
      self._repeats = self._repeats || {};
  
      var repeats = pickAndMergeParentArrays(self._repeats, selector);
  
      repeats.forEach(function(repeat) {
        var wrapper = '[' + tagPrefix + '-repeat-wrapper="' + repeat.id + '"]',
          data = self.get(repeat.selector),
          items = [];
  
        repeat.filter = repeat.filter || [];
        w.dom(wrapper).empty();
  
        for (var key in data) {
          /*
                  var item = data[key],
                          test = true;
                  for (var i in repeat.filter) {
                      var filterName = repeat.filter[i];
                      var filter = self._filters[filterName] || function(data) { return data };
                      test = filter(item);
                      if (!test) { break; }
                  }
                  if (!test) { continue; }
                  */
  
          w.dom(repeat.element).attr(tagPrefix + '-scope', key);
          var html = w.dom(repeat.element).get(0).outerHTML;
          html = html.replace(/\$\$key/gi, key);
          items.push(html);
        }
  
        w.dom(wrapper).html(items.join(''));
        self.registerBindings();
        self.updateBindings();
      });
    };
  
    ////////////////////////
    // DOM METHODS: FORMS //
    ////////////////////////
  
    WAY.prototype.updateForms = function() {
      // If we just parse the forms with form2js (see commits before 08/19/2014) and set the data with way.set(),
      // we reset the entire data for this pathkey in the datastore. It causes the bug
      // reported here: https://github.com/gwendall/way.js/issues/10
      // Solution:
      // 1. watch new forms with a [way-data] attribute
      // 2. remove this attribute
      // 3. attach the appropriate attributes to its child inputs
      // -> so that each input is set separately to way.js' datastore
  
      var self = this;
      var selector = 'form[' + tagPrefix + '-data]';
  
      var elements = w.dom(selector).get();
      for (var i in elements) {
        var form = elements[i],
          options = self.dom(form).getOptions(),
          formDataSelector = options.data;
        w.dom(form).removeAttr(tagPrefix + '-data');
  
        // Reverse needed to set the right index for "[]" names
        var inputs = w.dom(form).find('[name]').reverse().get();
        for (var i in inputs) {
          var input = inputs[i], name = w.dom(input).attr('name');
  
          if (endsWith(name, '[]')) {
            var array = name.split('[]')[0],
              arraySelector = "[name^='" + array + "']",
              arrayIndex = w.dom(form).find(arraySelector).get().length;
            name = array + '.' + arrayIndex;
          }
          var selector = formDataSelector + '.' + name;
          options.data = selector;
          self.dom(input).setOptions(options);
          w.dom(input).removeAttr('name');
        }
      }
    };
  
    /////////////////////////////////////////////
    // DOM METHODS: GET - SET ALL DEPENDENCIES //
    /////////////////////////////////////////////
  
    WAY.prototype.registerDependencies = function() {
      this.registerBindings();
      this.registerRepeats();
    };
  
    WAY.prototype.updateDependencies = function(selector) {
      this.updateBindings(selector);
      this.updateRepeats(selector);
      this.updateForms(selector);
    };
  
    //////////////////////////////////
    // DOM METHODS: OPTIONS PARSING //
    //////////////////////////////////
  
    WAY.prototype.setOptions = function(options, element) {
      var self = this, element = self._element || element;
  
      for (var k in options) {
        var attr = tagPrefix + '-' + k, value = options[k];
        w.dom(element).attr(attr, value);
      }
    };
  
    WAY.prototype.getOptions = function(element) {
      var self = this,
        element = element || self._element,
        defaultOptions = {
          data: null,
          html: false,
          readonly: false,
          writeonly: false,
          persistent: false
        };
      return _w.extend(defaultOptions, self.dom(element).getAttrs(tagPrefix));
    };
  
    WAY.prototype.getAttrs = function(prefix, element) {
      var self = this, element = element || self._element;
  
      var parseAttrValue = function(key, value) {
        var attrTypes = {
          pick: 'array',
          omit: 'array',
          readonly: 'boolean',
          writeonly: 'boolean',
          json: 'boolean',
          html: 'boolean',
          persistent: 'boolean'
        };
  
        var parsers = {
          array: function(value) {
            return value.split(',');
          },
          boolean: function(value) {
            if (value === 'true') {
              return true;
            }
            if (value === 'false') {
              return false;
            }
            return true;
          }
        };
        var defaultParser = function() {
          return value;
        };
        var valueType = attrTypes[key] || null;
        var parser = parsers[valueType] || defaultParser;
  
        return parser(value);
      };
  
      var attributes = {};
      var attrs = [].slice.call(w.dom(element).get(0).attributes);
      attrs.forEach(function(attr) {
        var include = prefix && startsWith(attr.name, prefix + '-')
          ? true
          : false;
        if (include) {
          var name = prefix
            ? attr.name.slice(prefix.length + 1, attr.name.length)
            : attr.name;
          var value = parseAttrValue(name, attr.value);
          if (_w.contains(['transform', 'filter'], name)) {
            value = value.split('|');
          }
          attributes[name] = value;
        }
      });
  
      return attributes;
    };
  
    //////////////////////////
    // DOM METHODS: SCOPING //
    //////////////////////////
  
    WAY.prototype.scope = function(options, element) {
      var self = this,
        element = element || self._element,
        scopeAttr = tagPrefix + '-scope',
        scopeBreakAttr = tagPrefix + '-scope-break',
        scopes = [],
        scope = '';
  
      var parentsSelector = '[' + scopeBreakAttr + '], [' + scopeAttr + ']';
      var elements = w.dom(element).parents(parentsSelector).get();
      for (var i in elements) {
        var el = elements[i];
        if (w.dom(el).attr(scopeBreakAttr)) {
          break;
        }
        var attr = w.dom(el).attr(scopeAttr);
        scopes.unshift(attr);
      }
      if (w.dom(element).attr(scopeAttr)) {
        scopes.push(w.dom(element).attr(scopeAttr));
      }
      if (w.dom(element).attr(scopeBreakAttr)) {
        scopes = [];
      }
  
      scope = _w.compact(scopes).join('.');
  
      return scope;
    };
  
    //////////////////
    // DATA METHODS //
    //////////////////
  
    WAY.prototype.get = function(selector) {
      var self = this;
      if (selector !== undefined && !_w.isString(selector)) {
        return false;
      }
      if (!self.data) {
        return {};
      }
      return selector ? _json.get(self.data, selector) : self.data;
    };
  
    WAY.prototype.set = function(selector, value, options) {
      if (!selector) {
        return false;
      }
      if (selector.split('.')[0] === 'this') {
        console.log('Sorry, "this" is a reserved word in way.js');
        return false;
      }
  
      var self = this;
      options = options || {};
  
      if (selector) {
        if (!_w.isString(selector)) {
          return false;
        }
        self.data = self.data || {};
        self.data = selector ? _json.set(self.data, selector, value) : {};
  
        self.updateDependencies(selector);
        self.emitChange(selector, value);
        if (options.persistent) {
          self.backup(selector);
        }
      }
    };
  
    WAY.prototype.push = function(selector, value, options) {
      if (!selector) {
        return false;
      }
  
      var self = this;
      options = options || {};
  
      if (selector) {
        self.data = selector ? _json.push(self.data, selector, value, true) : {};
      }
  
      self.updateDependencies(selector);
      self.emitChange(selector, null);
      if (options.persistent) {
        self.backup(selector);
      }
    };
  
    WAY.prototype.remove = function(selector, options) {
      var self = this;
      options = options || {};
  
      if (selector) {
        self.data = _json.remove(self.data, selector);
      } else {
        self.data = {};
      }
  
      self.updateDependencies(selector);
      self.emitChange(selector, null);
      if (options.persistent) {
        self.backup(selector);
      }
    };
  
    WAY.prototype.clear = function() {
      this.remove(null, { persistent: true });
    };
  
    //////////////////////////
    // LOCALSTORAGE METHODS //
    //////////////////////////
  
    WAY.prototype.backup = function() {
      var self = this;
      if (!self.options.persistent) {
        return;
      }
      try {
        var data = self.data || {};
        localStorage.setItem(tagPrefix, JSON.stringify(data));
      } catch (e) {
        console.log('Your browser does not support localStorage.');
      }
    };
  
    WAY.prototype.restore = function() {
      var self = this;
      if (!self.options.persistent) {
        return;
      }
      try {
        var data = localStorage.getItem(tagPrefix);
        try {
          data = JSON.parse(data);
          for (var key in data) {
            self.set(key, data[key]);
          }
        } catch (e) {}
      } catch (e) {
        console.log('Your browser does not support localStorage.');
      }
    };
  
    //////////
    // MISC //
    //////////
  
    var matchesSelector = function(el, selector) {
      var matchers = [
        'matches',
        'matchesSelector',
        'webkitMatchesSelector',
        'mozMatchesSelector',
        'msMatchesSelector',
        'oMatchesSelector'
      ],
        fn = null;
      for (var i in matchers) {
        fn = matchers[i];
        if (_w.isFunction(el[fn])) {
          return el[fn](selector);
        }
      }
      return false;
    };
  
    var startsWith = function(str, starts) {
      if (starts === '') {
        return true;
      }
      if (str === null || starts === null) {
        return false;
      }
      str = String(str);
      starts = String(starts);
      return (
        str.length >= starts.length && str.slice(0, starts.length) === starts
      );
    };
  
    var endsWith = function(str, ends) {
      if (ends === '') {
        return true;
      }
      if (str === null || ends === null) {
        return false;
      }
      str = String(str);
      ends = String(ends);
      return (
        str.length >= ends.length &&
        str.slice(str.length - ends.length, str.length) === ends
      );
    };
  
    var cleanEmptyKeys = function(object) {
      return _w.pick(object, _w.compact(_w.keys(object)));
    };
  
    var filterStartingWith = function(object, string, type) {
      // true: pick - false: omit
  
      var keys = _w.keys(object);
      keys.forEach(function(key) {
        if (type) {
          if (!startsWith(key, string)) {
            delete object[key];
          }
        } else {
          if (startsWith(key, string)) {
            delete object[key];
          }
        }
      });
      return object;
    };
  
    var selectNested = function(data, keys, type) {
      // true: pick - false: omit
  
      // Flatten / unflatten to allow for nested picks / omits (doesn't work with regular pick)
      // ex:  data = {something:{nested:"value"}}
      //		keys = ['something.nested']
  
      var flat = _json.flatten(data);
      for (var i in keys)
        flat = filterStartingWith(flat, keys[i], type);
      var unflat = _json.unflatten(flat);
      // Unflatten returns an object with an empty property if it is given an empty object
      return cleanEmptyKeys(unflat);
    };
  
    var pickAndMergeParentArrays = function(object, selector) {
      // Example:
      // object = { a: [1,2,3], a.b: [4,5,6], c: [7,8,9] }
      // fn(object, "a.b")
      // > [1,2,3,4,5,6]
  
      var keys = [];
      if (selector) {
        // Set bindings for the specified selector
  
        // (bindings that are repeat items)
        var split = selector.split('.'),
          lastKey = split[split.length - 1],
          isArrayItem = !isNaN(lastKey);
  
        if (isArrayItem) {
          split.pop();
          var key = split.join('.');
          keys = object[key] ? _w.union(keys, object[key]) : keys;
        }
  
        // (bindings with keys starting with, to include nested bindings)
        for (var key in object) {
          if (startsWith(key, selector)) {
            keys = _w.union(keys, object[key]);
          }
        }
      } else {
        // Set bindings for all selectors
        for (var key in object) {
          keys = _w.union(keys, object[key]);
        }
      }
      return keys;
    };
  
    var isPrintableKey = function(e) {
      var keycode = e.keyCode;
      if (!keycode) {
        return true;
      }
  
      var valid =
        keycode === 8 || // delete
        (keycode > 47 && keycode < 58) || // number keys
        keycode === 32 ||
        keycode === 13 || // spacebar & return key(s) (if you want to allow carriage returns)
        (keycode > 64 && keycode < 91) || // letter keys
        (keycode > 95 && keycode < 112) || // numpad keys
        (keycode > 185 && keycode < 193) || // ;=,-./` (in order)
        (keycode > 218 && keycode < 223); // [\]' (in order)
  
      return valid;
    };
  
    var escapeHTML = function(str) {
      return str && _w.isString(str)
        ? str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
        : str;
    };
  
    ///////////////////////////////////////////////////
    // _w (strip of the required underscore methods) //
    ///////////////////////////////////////////////////
  
    var _w = {};
  
    var ArrayProto = Array.prototype,
      ObjProto = Object.prototype,
      FuncProto = Function.prototype;
  
    var nativeIsArray = Array.isArray,
      nativeKeys = Object.keys,
      nativeBind = FuncProto.bind;
  
    var push = ArrayProto.push,
      slice = ArrayProto.slice,
      concat = ArrayProto.concat,
      toString = ObjProto.toString,
      hasOwnProperty = ObjProto.hasOwnProperty;
  
    var flatten = function(input, shallow, strict, output) {
      if (shallow && _w.every(input, _w.isArray)) {
        return concat.apply(output, input);
      }
      for (var i = 0, length = input.length; i < length; i++) {
        var value = input[i];
        if (!_w.isArray(value) && !_w.isArguments(value)) {
          if (!strict) output.push(value);
        } else if (shallow) {
          push.apply(output, value);
        } else {
          flatten(value, shallow, strict, output);
        }
      }
      return output;
    };
  
    var createCallback = function(func, context, argCount) {
      if (context === void 0) return func;
      switch (argCount == null ? 3 : argCount) {
        case 1:
          return function(value) {
            return func.call(context, value);
          };
        case 2:
          return function(value, other) {
            return func.call(context, value, other);
          };
        case 3:
          return function(value, index, collection) {
            return func.call(context, value, index, collection);
          };
        case 4:
          return function(accumulator, value, index, collection) {
            return func.call(context, accumulator, value, index, collection);
          };
      }
      return function() {
        return func.apply(context, arguments);
      };
    };
  
    _w.compact = function(array) {
      return _w.filter(array, _w.identity);
    };
  
    _w.filter = function(obj, predicate, context) {
      var results = [];
      if (obj == null) return results;
      predicate = _w.iteratee(predicate, context);
      _w.each(obj, function(value, index, list) {
        if (predicate(value, index, list)) results.push(value);
      });
      return results;
    };
  
    _w.identity = function(value) {
      return value;
    };
  
    _w.every = function(obj, predicate, context) {
      if (obj == null) return true;
      predicate = _w.iteratee(predicate, context);
      var keys = obj.length !== +obj.length && _w.keys(obj),
        length = (keys || obj).length,
        index,
        currentKey;
      for (index = 0; index < length; index++) {
        currentKey = keys ? keys[index] : index;
        if (!predicate(obj[currentKey], currentKey, obj)) return false;
      }
      return true;
    };
  
    _w.union = function() {
      return _w.uniq(flatten(arguments, true, true, []));
    };
  
    _w.uniq = function(array, isSorted, iteratee, context) {
      if (array == null) return [];
      if (!_w.isBoolean(isSorted)) {
        context = iteratee;
        iteratee = isSorted;
        isSorted = false;
      }
      if (iteratee != null) iteratee = _w.iteratee(iteratee, context);
      var result = [];
      var seen = [];
      for (var i = 0, length = array.length; i < length; i++) {
        var value = array[i];
        if (isSorted) {
          if (!i || seen !== value) result.push(value);
          seen = value;
        } else if (iteratee) {
          var computed = iteratee(value, i, array);
          if (_w.indexOf(seen, computed) < 0) {
            seen.push(computed);
            result.push(value);
          }
        } else if (_w.indexOf(result, value) < 0) {
          result.push(value);
        }
      }
      return result;
    };
  
    _w.pick = function(obj, iteratee, context) {
      var result = {}, key;
      if (obj == null) return result;
      if (_w.isFunction(iteratee)) {
        iteratee = createCallback(iteratee, context);
        for (key in obj) {
          var value = obj[key];
          if (iteratee(value, key, obj)) result[key] = value;
        }
      } else {
        var keys = concat.apply([], slice.call(arguments, 1));
        obj = new Object(obj);
        for (var i = 0, length = keys.length; i < length; i++) {
          key = keys[i];
          if (key in obj) result[key] = obj[key];
        }
      }
      return result;
    };
  
    _w.has = function(obj, key) {
      return obj != null && hasOwnProperty.call(obj, key);
    };
  
    _w.keys = function(obj) {
      if (!_w.isObject(obj)) return [];
      if (nativeKeys) return nativeKeys(obj);
      var keys = [];
      for (var key in obj)
        if (_w.has(obj, key)) keys.push(key);
      return keys;
    };
  
    _w.contains = function(obj, target) {
      if (obj == null) return false;
      if (obj.length !== +obj.length) obj = _w.values(obj);
      return _w.indexOf(obj, target) >= 0;
    };
  
    _w.sortedIndex = function(array, obj, iteratee, context) {
      iteratee = _w.iteratee(iteratee, context, 1);
      var value = iteratee(obj);
      var low = 0, high = array.length;
      while (low < high) {
        var mid = (low + high) >>> 1;
        if (iteratee(array[mid]) < value) low = mid + 1;
        else high = mid;
      }
      return low;
    };
  
    _w.property = function(key) {
      return function(obj) {
        return obj[key];
      };
    };
  
    _w.iteratee = function(value, context, argCount) {
      if (value == null) return _w.identity;
      if (_w.isFunction(value)) return createCallback(value, context, argCount);
      if (_w.isObject(value)) return _w.matches(value);
      return _w.property(value);
    };
  
    _w.pairs = function(obj) {
      var keys = _w.keys(obj);
      var length = keys.length;
      var pairs = Array(length);
      for (var i = 0; i < length; i++) {
        pairs[i] = [keys[i], obj[keys[i]]];
      }
      return pairs;
    };
  
    _w.matches = function(attrs) {
      var pairs = _w.pairs(attrs), length = pairs.length;
      return function(obj) {
        if (obj == null) return !length;
        obj = new Object(obj);
        for (var i = 0; i < length; i++) {
          var pair = pairs[i], key = pair[0];
          if (pair[1] !== obj[key] || !(key in obj)) return false;
        }
        return true;
      };
    };
  
    _w.indexOf = function(array, item, isSorted) {
      if (array == null) return -1;
      var i = 0, length = array.length;
      if (isSorted) {
        if (typeof isSorted == 'number') {
          i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
        } else {
          i = _w.sortedIndex(array, item);
          return array[i] === item ? i : -1;
        }
      }
      for (; i < length; i++)
        if (array[i] === item) return i;
      return -1;
    };
  
    _w.values = function(obj) {
      var keys = _w.keys(obj);
      var length = keys.length;
      var values = Array(length);
      for (var i = 0; i < length; i++) {
        values[i] = obj[keys[i]];
      }
      return values;
    };
  
    _w.extend = function(obj) {
      if (!_w.isObject(obj)) return obj;
      var source, prop;
      for (var i = 1, length = arguments.length; i < length; i++) {
        source = arguments[i];
        for (prop in source) {
          if (hasOwnProperty.call(source, prop)) {
            obj[prop] = source[prop];
          }
        }
      }
      return obj;
    };
  
    _w.isArray = function(obj) {
      return toString.call(obj) === '[object Array]';
    };
  
    _w.isBoolean = function(obj) {
      return (
        obj === true || obj === false || toString.call(obj) === '[object Boolean]'
      );
    };
  
    _w.isUndefined = function(obj) {
      return obj === void 0;
    };
  
    _w.isObject = function(obj) {
      var type = typeof obj;
      return type === 'function' || (type === 'object' && !!obj);
    };
  
    _w.each = function(obj, iteratee, context) {
      if (obj == null) return obj;
      iteratee = createCallback(iteratee, context);
      var i, length = obj.length;
      if (length === +length) {
        for (i = 0; i < length; i++) {
          iteratee(obj[i], i, obj);
        }
      } else {
        var keys = _w.keys(obj);
        for ((i = 0), (length = keys.length); i < length; i++) {
          iteratee(obj[keys[i]], keys[i], obj);
        }
      }
      return obj;
    };
  
    _w.each(
      ['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'],
      function(name) {
        _w['is' + name] = function(obj) {
          return toString.call(obj) === '[object ' + name + ']';
        };
      }
    );
  
    ///////////////////////////////////////////////////////////
    // _json (strip of the required underscore.json methods) //
    ///////////////////////////////////////////////////////////
  
    var deepJSON = function(obj, key, value, remove) {
      var keys = key
        .replace(/\[(["']?)([^\1]+?)\1?\]/g, '.$2')
        .replace(/^\./, '')
        .split('.'),
        root,
        i = 0,
        n = keys.length;
  
      // Set deep value
      if (arguments.length > 2) {
        root = obj;
        n--;
  
        while (i < n) {
          key = keys[i++];
          obj = obj[key] = _w.isObject(obj[key]) ? obj[key] : {};
        }
  
        if (remove) {
          if (_w.isArray(obj)) {
            obj.splice(keys[i], 1);
          } else {
            delete obj[keys[i]];
          }
        } else {
          obj[keys[i]] = value;
        }
  
        value = root;
  
        // Get deep value
      } else {
        while ((obj = obj[keys[i++]]) != null && i < n) {
        }
        value = i < n ? void 0 : obj;
      }
  
      return value;
    };
  
    var _json = {};
  
    _json.VERSION = '0.1.0';
    _json.debug = true;
  
    _json.exit = function(source, reason, data, value) {
      if (!_json.debug) return;
  
      var messages = {};
      messages.noJSON = 'Not a JSON';
      messages.noString = 'Not a String';
      messages.noArray = 'Not an Array';
      messages.missing = 'Missing argument';
  
      var error = { source: source, data: data, value: value };
      error.message = messages[reason]
        ? messages[reason]
        : 'No particular reason';
      console.log('Error', error);
      return;
    };
  
    _json.is = function(json) {
      return toString.call(json) == '[object Object]';
    };
  
    _json.isStringified = function(string) {
      var test = false;
      try {
        test = /^[\],:{}\s]*$/.test(
          string
            .replace(/\\["\\\/bfnrtu]/g, '@')
            .replace(
              /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
              ']'
            )
            .replace(/(?:^|:|,)(?:\s*\[)+/g, '')
        );
      } catch (e) {}
      return test;
    };
  
    _json.get = function(json, selector) {
      if (json == undefined) return _json.exit('get', 'missing', 'json', json);
      if (selector == undefined)
        return _json.exit('get', 'missing', 'selector', selector);
      if (!_w.isString(selector))
        return _json.exit('get', 'noString', 'selector', selector);
      return deepJSON(json, selector);
    };
  
    _json.set = function(json, selector, value) {
      if (json == undefined) return _json.exit('set', 'missing', 'json', json);
      if (selector == undefined)
        return _json.exit('set', 'missing', 'selector', selector);
      if (!_w.isString(selector))
        return _json.exit('set', 'noString', 'selector', selector);
      return value
        ? deepJSON(json, selector, value)
        : _json.remove(json, selector);
      // return deepJSON(json, selector, value); // Now removes the property if the value is empty. Maybe should keep it instead?
    };
  
    _json.remove = function(json, selector) {
      if (json == undefined) return _json.exit('remove', 'missing', 'json', json);
      if (selector == undefined)
        return _json.exit('remove', 'missing', 'selector', selector);
      if (!_w.isString(selector))
        return _json.exit('remove', 'noString', 'selector', selector);
      return deepJSON(json, selector, null, true);
    };
  
    _json.push = function(json, selector, value, force) {
      if (json == undefined) return _json.exit('push', 'missing', 'json', json);
      if (selector == undefined)
        return _json.exit('push', 'missing', 'selector', selector);
      var array = _json.get(json, selector);
      if (!_w.isArray(array)) {
        if (force) {
          array = [];
        } else {
          return _json.exit('push', 'noArray', 'array', array);
        }
      }
      array.push(value);
      return _json.set(json, selector, array);
    };
  
    _json.unshift = function(json, selector, value) {
      if (json == undefined)
        return _json.exit('unshift', 'missing', 'json', json);
      if (selector == undefined)
        return _json.exit('unshift', 'missing', 'selector', selector);
      if (value == undefined)
        return _json.exit('unshift', 'missing', 'value', value);
      var array = _json.get(json, selector);
      if (!_w.isArray(array))
        return _json.exit('unshift', 'noArray', 'array', array);
      array.unshift(value);
      return _json.set(json, selector, array);
    };
  
    _json.flatten = function(json) {
      if (json.constructor.name != 'Object')
        return _json.exit('flatten', 'noJSON', 'json', json);
  
      var result = {};
      function recurse(cur, prop) {
        if (Object(cur) !== cur) {
          result[prop] = cur;
        } else if (Array.isArray(cur)) {
          for (var i = 0, l = cur.length; i < l; i++) {
            recurse(cur[i], prop ? prop + '.' + i : '' + i);
            if (l == 0) result[prop] = [];
          }
        } else {
          var isEmpty = true;
          for (var p in cur) {
            isEmpty = false;
            recurse(cur[p], prop ? prop + '.' + p : p);
          }
          if (isEmpty) result[prop] = {};
        }
      }
      recurse(json, '');
      return result;
    };
  
    _json.unflatten = function(data) {
      if (Object(data) !== data || Array.isArray(data)) return data;
      var result = {}, cur, prop, idx, last, temp;
      for (var p in data) {
        (cur = result), (prop = ''), (last = 0);
        do {
          idx = p.indexOf('.', last);
          temp = p.substring(last, idx !== -1 ? idx : undefined);
          cur = cur[prop] || (cur[prop] = !isNaN(parseInt(temp)) ? [] : {});
          prop = temp;
          last = idx + 1;
        } while (idx >= 0);
        cur[prop] = data[p];
      }
      return result[''];
    };
  
    _json.prettyprint = function(json) {
      return JSON.stringify(json, undefined, 2);
    };
  
    //////////////////////////////////////////
    // wQuery (mini replacement for jQuery) //
    //////////////////////////////////////////
  
    var wQuery = function() {};
    wQuery.constructor = wQuery;
  
    wQuery.prototype.dom = function(selector, createOptions) {
      var self = this, elements = [];
  
      if (createOptions) {
        var element = document.createElement(selector);
        for (var k in createOptions) {
          element[k] = createOptions[k];
        }
      } else {
        if (_w.isString(selector)) {
          elements = [].slice.call(document.querySelectorAll(selector));
        } else {
          if (_w.isObject(selector) && selector.attributes) {
            elements = [selector];
          }
        }
        self._elements = elements;
        self.length = elements.length;
        return self;
      }
    };
  
    wQuery.prototype.on = function(events, fn) {
      var self = this, elements = self._elements;
      events = events.split(' ');
      for (var i = 0, lenEl = elements.length; i < lenEl; i++) {
        var element = elements[i];
        for (var j = 0, lenEv = events.length; j < lenEv; j++) {
          if (element.addEventListener) {
            element.addEventListener(events[j], fn, false);
          }
        }
      }
    };
  
    wQuery.prototype.find = function(selector) {
      var self = this, element = self.get(0), elements = [];
  
      if (_w.isString(selector)) {
        elements = [].slice.call(element.querySelectorAll(selector));
      }
      self._elements = elements;
      return self;
    };
  
    wQuery.prototype.get = function(index, chain) {
      var self = this,
        elements = self._elements || [],
        element = elements[index] || {};
  
      if (chain) {
        self._element = element;
        return self;
      } else {
        return _w.isNumber(index) ? element : elements;
      }
    };
  
    wQuery.prototype.reverse = function() {
      this._elements = this._elements.reverse();
      return this;
    };
  
    wQuery.prototype.val = function(value) {
      return this.prop('value', value);
    };
  
    wQuery.prototype.type = function(value) {
      return this.prop('type', value);
    };
  
    wQuery.prototype.html = function(value) {
      return this.prop('innerHTML', value);
    };
  
    wQuery.prototype.text = function(value) {
      return this.prop('innerHTML', escapeHTML(value));
    };
  
    wQuery.prototype.prop = function(prop, value) {
      var self = this, elements = self._elements;
  
      for (var i in elements) {
        if (_w.isUndefined(value)) {
          return elements[i][prop];
        } else {
          elements[i][prop] = value;
        }
      }
    };
  
    wQuery.prototype.attr = function(attr, value) {
      var self = this, elements = self._elements;
      for (var i in elements) {
        if (value === undefined) {
          return elements[i].getAttribute(attr);
        } else {
          elements[i].setAttribute(attr, value);
        }
      }
      return self;
    };
  
    wQuery.prototype.removeAttr = function(attr) {
      var self = this;
      for (var i in self._elements)
        self._elements[i].removeAttribute(attr);
      return self;
    };
  
    wQuery.prototype.addClass = function(c) {
      var self = this;
      for (var i in self._elements)
        self._elements[i].classList.add(c);
      return self;
    };
  
    wQuery.prototype.removeClass = function(c) {
      var self = this;
      for (var i in self._elements)
        self._elements[i].classList.remove(c);
      return self;
    };
  
    wQuery.prototype.parents = function(selector) {
      var self = this,
        element = self.get(0),
        parent = element.parentNode,
        parents = [];
  
      while (parent !== null) {
        var o = parent,
          matches = matchesSelector(o, selector),
          isNotDomRoot = o.doctype === undefined ? true : false;
        if (!selector) {
          matches = true;
        }
        if (matches && isNotDomRoot) {
          parents.push(o);
        }
        parent = o.parentNode;
      }
      self._elements = parents;
      return self;
    };
  
    wQuery.prototype.parent = function(selector) {
      var self = this,
        element = self.get(0),
        o = element.parentNode,
        matches = matchesSelector(o, selector);
      if (!selector) {
        matches = true;
      }
      return matches ? o : {};
    };
  
    wQuery.prototype.clone = function(chain) {
      var self = this, element = self.get(0), clone = element.cloneNode(true);
      self._elements = [clone];
      return chain ? self : clone;
    };
  
    wQuery.prototype.empty = function(chain) {
      var self = this, element = self.get(0);
      if (!element || !element.hasChildNodes) {
        return chain ? self : element;
      }
  
      while (element.hasChildNodes()) {
        element.removeChild(element.lastChild);
      }
      return chain ? self : element;
    };
  
    wQuery.prototype.replaceWith = function(newDOM) {
      var self = this, oldDOM = self.get(0), parent = oldDOM.parentNode;
      parent.replaceChild(newDOM, oldDOM);
    };
  
    wQuery.prototype.ready = function(callback) {
      if (document && _w.isFunction(document.addEventListener)) {
        document.addEventListener('DOMContentLoaded', callback, false);
      } else if (window && _w.isFunction(window.addEventListener)) {
        window.addEventListener('load', callback, false);
      } else {
        document.onreadystatechange = function() {
          if (document.readyState === 'complete') {
            callback();
          }
        };
      }
    };
  
    //////////////////////
    // WATCH DOM EVENTS //
    //////////////////////
  
    way = new WAY();
  
    var timeoutInput = null;
    var eventInputChange = function(e) {
      if (timeoutInput) {
        clearTimeout(timeoutInput);
      }
      timeoutInput = setTimeout(function() {
        var element = w.dom(e.target).get(0);
        way.dom(element).toStorage();
      }, way.options.timeout);
    };
  
    var eventClear = function(e) {
      e.preventDefault();
      var options = way.dom(this).getOptions();
      way.remove(options.data, options);
    };
  
    var eventPush = function(e) {
      e.preventDefault();
      var options = way.dom(this).getOptions();
      if (!options || !options['action-push']) {
        return false;
      }
      var split = options['action-push'].split(':'),
        selector = split[0] || null,
        value = split[1] || null;
      way.push(selector, value, options);
    };
  
    var eventRemove = function(e) {
      e.preventDefault();
      var options = way.dom(this).getOptions();
      if (!options || !options['action-remove']) {
        return false;
      }
      way.remove(options['action-remove'], options);
    };
  
    var timeoutDOM = null;
    var eventDOMChange = function() {
      // We need to register dynamically added bindings so we do it by watching DOM changes
      // We use a timeout since "DOMSubtreeModified" gets triggered on every change in the DOM (even input value changes)
      // so we can limit the number of scans when a user is typing something
      if (timeoutDOM) {
        clearTimeout(timeoutDOM);
      }
      timeoutDOM = setTimeout(function() {
        way.registerDependencies();
        setEventListeners();
      }, way.options.timeoutDOM);
    };
  
    //////////////
    // INITIATE //
    //////////////
  
    w = new wQuery();
    way.w = w;
  
    var setEventListeners = function() {
      w.dom('body').on('DOMSubtreeModified', eventDOMChange);
      w.dom('[' + tagPrefix + '-data]').on('input change', eventInputChange);
      w.dom('[' + tagPrefix + '-clear]').on('click', eventClear);
      w.dom('[' + tagPrefix + '-action-remove]').on('click', eventRemove);
      w.dom('[' + tagPrefix + '-action-push]').on('click', eventPush);
    };
  
    var eventInit = function() {
      setEventListeners();
      way.restore();
      way.setDefaults();
      way.registerDependencies();
      way.updateDependencies();
    };
  
    w.ready(eventInit);
  
    return way;
});